<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="app">
    <input type="text" name="" id="" v-model="aaa" >===={{aaa}}
  </div>
  <script>
    class _vue {
      constructor(options){
        this.$options = options
        this.$data = options.data
        this.$el = options.el

        // 将 data 添加到响应式系统中
        new Observer(this.$data)

        // 代理 this.$data 的数据=====首次
        Object.keys(this.$data).forEach((key) => {
          this._proxy(key)
        })

        new Compiler(this.$el, this)
      }

      _proxy(key) {
        Object.defineProperty(this, key, {
          // 属性是否可配置
          configurable: true,
          // 是否可枚举
          enumerable: true,
          set(newValue) {
            this.$data[key] = newValue
          },
          get() {
            return this.$data[key]
          }
        })
      }
    }

    // 劫持 data 数据
    class Observer {
      constructor(data) {
        this.data = data
        Object.keys(data).forEach(key => {
          // 封装一层
          this.defineReactive(this.data, key, data[key])
        })
      }
      defineReactive(data, key, value) {
        // 一个 key 属性对应一个 Dep 对象
        Object.defineProperty(data, key, {
          enumerable: true,
          configurable: true,
          set(newValue) {
            // 如果值未变，就不操作
            if (newValue === value) {
              return
            }
            // 更改值
            value = newValue
            // 通知 watcher 更新值====== notify 内部调用 watcher 的 update 方法更新视图
            dep.notify()
          },
          get() {
            // 如果是视图层使用的数据触发 get 函数，就把对应的 watcher 添加到 dep 的 subs 中
            if (Dep.target) {
              dep.addSub(Dep.target)
            }
            return value
          },
        })
      }
    }

    class Dep {
      constructor() {
        this.subs = []
      }
      // 添加 watcher 到 subs 中
      addSub(sub) {
        this.subs.push(sub)
      }
      // 调用 notify 函数通知 watcher =======通过 update 函数的调用通知
      notify() {
        this.subs.forEach(sub => {
          sub.update()
        })
      }
    }
    let dep = new Dep()
    class Watcher {
      constructor(node, name, vm) {
        this.node = node
        this.name = name
        this.vm = vm
        // 
        Dep.target = this
        this.update()
        this.target = null
      }
      update() {
        this.node.nodeValue = this.vm[this.name]
      }
    }
    
    // 利用正则表达式来进行查找使用过插值表达式的节点
    const reg = /\{\{(.+)\}\}/
    class Compiler {
      constructor(el, vm) {
        this.el = document.querySelector(el)
        this.vm = vm
        // 进行遍历标签节点，查找出使用插值表达式的 DOM 节点
        this.frag = this._createFragment()
        // 由于上面使用 appendChid 方法将原 dom 树中的节点添加到 DocumentFragment 中会导致节点丢失，所以要把 frag 添加到 el 中
        this.el.appendChild(this.frag)
      }
      _createFragment() {
        // 创建虚拟节对象（在内存中）
        const frag = document.createDocumentFragment()

        let child
        // 遍历 el 上的所有标签的 DOM 对象
        while (child = this.el.firstChild) {
          this._compile(child)
          frag.appendChild(child)
        }
        // 因为 createDocumentFragment 在内存中创建节点，会导致每添加添加都会丢失之前的 child，所以用 frag 来保存并返回
        return frag
      }
      // 进行遍历节点属性来查询使用过插值表达式 {{}} 和 v-model 的节点
      // 如果节点 nodeType === 1 就是标签属性
      // 如果节点 nodeType === 3 就是文本属性
      _compile(node) {
        // 判断当前节点循环出的是不是标签节点
        if (node.nodeType === 1) {
          const attrs = node.attributes
          if (attrs.hasOwnProperty('v-model')) {
            // 当前节点属性是 v-model
            const name = attrs['v-model'].nodeValue
            node.addEventListener('input', e => {
              this.vm[name] = e.target.value
            })
          }
        }
        // 判断当前节点循环出的是不是文本节点
        if (node.nodeType === 3) {
          if (reg.test(node.nodeValue)) {
            const name = RegExp.$1.trim()
            new Watcher(node, name, this.vm)
          }
        }
      }
    }
  </script>

  <script>
   const app =  new _vue({
      el: '#app',
      data: {
        aaa: 111
      }
    })
  </script>
</body>
</html>